﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ValidationScope.cs" company="Spenta Consulting">
//   (c) Copyright 2011
// </copyright>
// <summary>
//   Defines the ValidationScope type. Taken from http://www.thejoyofcode.com/Silverlight_Validation_and_MVVM_Part_II.aspx
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Spenta.StreetCare.Silverlight.MVVM.Validations
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using System.Reflection;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media;

    /// <summary>
    /// Validation scope class.
    /// </summary>
    public class ValidationScope
    {
        /// <summary>
        /// Validate bound property
        /// </summary>
        public static readonly DependencyProperty ValidateBoundPropertyProperty =
            DependencyProperty.RegisterAttached("ValidateBoundProperty", typeof(string), typeof(ValidationScope), new PropertyMetadata(null));

        /// <summary>
        /// Validation scope property
        /// </summary>
        public static readonly DependencyProperty ValidationScopeProperty =
            DependencyProperty.RegisterAttached("ValidationScope", typeof(ValidationScope), typeof(ValidationScope), new PropertyMetadata(null, ValidationScopeChanged));

        /// <summary>
        /// Private store for the errors
        /// </summary>
        private readonly ObservableCollection<ValidationError> errors = new ObservableCollection<ValidationError>();

        /// <summary>
        /// Gets the scope element.
        /// </summary>
        /// <value>The scope element.</value>
        public FrameworkElement ScopeElement { get; private set; }

        /// <summary>
        /// Gets the errors.
        /// </summary>
        /// <value>The errors.</value>
        public ObservableCollection<ValidationError> Errors
        {
            get { return this.errors; }
        }

        /// <summary>
        /// Gets the validate bound property.
        /// </summary>
        /// <param name="obj">The specified object.</param>
        /// <returns>The validate bound property.</returns>
        public static string GetValidateBoundProperty(DependencyObject obj)
        {
            return (string)obj.GetValue(ValidateBoundPropertyProperty);
        }

        /// <summary>
        /// Sets the validate bound property.
        /// </summary>
        /// <param name="obj">The specified object.</param>
        /// <param name="value">The specified value.</param>
        public static void SetValidateBoundProperty(DependencyObject obj, string value)
        {
            obj.SetValue(ValidateBoundPropertyProperty, value);
        }

        /// <summary>
        /// Gets the validation scope.
        /// </summary>
        /// <param name="obj">The specified object.</param>
        /// <returns>The validation scope</returns>
        public static ValidationScope GetValidationScope(DependencyObject obj)
        {
            return (ValidationScope)obj.GetValue(ValidationScopeProperty);
        }

        /// <summary>
        /// Sets the validation scope.
        /// </summary>
        /// <param name="obj">The specified object.</param>
        /// <param name="value">The value.</param>
        public static void SetValidationScope(DependencyObject obj, ValidationScope value)
        {
            obj.SetValue(ValidationScopeProperty, value);
        }

        /// <summary>
        /// Registers the binding error events.
        /// </summary>
        /// <param name="root">The root object.</param>
        /// <param name="scope">The scope.</param>
        public static void RegisterBindingErrorEvents(DependencyObject root, ValidationScope scope)
        {
            ForEachElement(
                root,
                delegate(DependencyObject obj)
                {
                    if (HasChildrenValidateBound(obj))
                    {
                        var element = (FrameworkElement)obj;
                        element.BindingValidationError +=
                                scope.ScopeElementBindingValidationError;
                    }
                });
        }

        /// <summary>
        /// Determines whether [has children validate bound] [the specified root].
        /// </summary>
        /// <param name="root">The root object.</param>
        /// <returns>
        ///    <c>true</c> if [has children validate bound] [the specified root]; otherwise, <c>false</c>.
        /// </returns>
        public static bool HasChildrenValidateBound(DependencyObject root)
        {
            int childCount = VisualTreeHelper.GetChildrenCount(root);
            if (childCount > 0)
            {
                for (int i = 0; i < childCount; i++)
                {
                    var obj = VisualTreeHelper.GetChild(root, i);
                    if (HasValidateBound(obj))
                    {
                        return true;
                    }
                }
            }

            if (root is TabControl)
            {
                if (((TabControl)root).Items.Cast<TabItem>().Any(HasValidateBound))
                {
                    return true;
                }
            }

            if (root is ContentControl)
            {
                if (((ContentControl)root).Content as DependencyObject != null)
                {
                    if (HasValidateBound((DependencyObject)((ContentControl)root).Content))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Determines whether [has validate bound] [the specified obj].
        /// </summary>
        /// <param name="obj">The object.</param>
        /// <returns>
        ///    <c>true</c> if [has validate bound] [the specified obj]; otherwise, <c>false</c>.
        /// </returns>
        public static bool HasValidateBound(DependencyObject obj)
        {
            // TODO - some of this reflection could be cached to improve performance
            var propertyName = GetValidateBoundProperty(obj);
            if (string.IsNullOrEmpty(propertyName))
            {
                return false;
            }

            var element = (FrameworkElement)obj;
            var field =
                element.GetType().GetFields(BindingFlags.Static | BindingFlags.FlattenHierarchy | BindingFlags.Public).
                    Where(p => p.FieldType == typeof(DependencyProperty) && p.Name == (propertyName + "Property")).
                    FirstOrDefault();

            return field != null;

            // {
            // throw new ArgumentException(
            // string.Format(
            // "Dependency property '{0}' could not be found on type '{1}'; ValidationScope.ValidateBoundProperty",
            // propertyName,
            // element.GetType()));
            // }
        }

        /// <summary>
        /// Registers the binding error events.
        /// </summary>
        /// <param name="root">The root object.</param>
        /// <param name="scope">The scope.</param>
        public static void UnregisterBindingErrorEvents(DependencyObject root, ValidationScope scope)
        {
            ForEachElement(
                root,
                delegate(DependencyObject obj)
                {
                    if (HasChildrenValidateBound(obj))
                    {
                        var element = (FrameworkElement)obj;
                        element.BindingValidationError -=
                                scope.ScopeElementBindingValidationError;
                    }
                });
        }

        /// <summary>
        /// Determines whether this instance is valid.
        /// </summary>
        /// <returns>
        ///    <c>true</c> if this instance is valid; otherwise, <c>false</c>.
        /// </returns>
        public bool IsValid()
        {
            return this.errors.Count == 0;
        }

        /// <summary>
        /// Validates the scope.
        /// </summary>
        public void ValidateScope()
        {
            ForEachElement(
                this.ScopeElement,
                delegate(DependencyObject obj)
                {
                    // TODO - some of this reflection could be cached to improve performance
                    string propertyName = GetValidateBoundProperty(obj);
                    if (!string.IsNullOrEmpty(propertyName))
                    {
                        var element = (FrameworkElement)obj;
                        var field = element.GetType().GetFields(BindingFlags.Static | BindingFlags.FlattenHierarchy | BindingFlags.Public)
                            .Where(p => p.FieldType == typeof(DependencyProperty) && p.Name == (propertyName + "Property"))
                            .FirstOrDefault();

                        if (field == null)
                        {
                            throw new ArgumentException(
                                string.Format(
                                    "Dependency property '{0}' could not be found on type '{1}'; ValidationScope.ValidateBoundProperty",
                                    propertyName,
                                    element.GetType()));
                        }

                        var be = element.GetBindingExpression((DependencyProperty)field.GetValue(null));
                        if (be != null)
                        {
                            be.UpdateSource();
                        }
                    }
                });
        }

        /// <summary>
        /// For each element.
        /// </summary>
        /// <param name="root">The root object.</param>
        /// <param name="action">The action.</param>
        private static void ForEachElement(DependencyObject root, Action<DependencyObject> action)
        {
            var processedObjects = new List<DependencyObject>();
            int childCount = VisualTreeHelper.GetChildrenCount(root);
            if (childCount > 0)
            {
                for (int i = 0; i < childCount; i++)
                {
                    var obj = VisualTreeHelper.GetChild(root, i);
                    action(obj);
                    ForEachElement(obj, action);
                    processedObjects.Add(obj);
                }
            }

            if (root is TabControl)
            {
                foreach (TabItem item in ((TabControl)root).Items)
                {
                    if (processedObjects.Contains(item))
                    {
                        continue;
                    }

                    action((DependencyObject)item);
                    ForEachElement(item, action);
                }
            }

            if (root is ContentControl)
            {
                if (((ContentControl)root).Content as DependencyObject != null)
                {
                    if (!processedObjects.Contains((DependencyObject)((ContentControl)root).Content))
                    {
                        action((DependencyObject)((ContentControl)root).Content);
                        ForEachElement((DependencyObject)((ContentControl)root).Content, action);
                    }
                }
            }
        }

        /// <summary>
        /// Handles the validate scope changed.
        /// </summary>
        /// <param name="source">The source.</param>
        /// <param name="args">The <see cref="System.Windows.DependencyPropertyChangedEventArgs"/> instance containing the event data.</param>
        private static void ValidationScopeChanged(DependencyObject source, DependencyPropertyChangedEventArgs args)
        {
            var oldScope = args.OldValue as ValidationScope;
            if (oldScope != null)
            {
                UnregisterBindingErrorEvents(oldScope.ScopeElement, oldScope);
                oldScope.ScopeElement.BindingValidationError -= oldScope.ScopeElementBindingValidationError;
                oldScope.ScopeElement = null;
            }

            var scopeElement = source as FrameworkElement;
            if (scopeElement == null)
            {
                throw new ArgumentException(string.Format(
                    "'{0}' is not a valid type.ValidationScope attached property can only be specified on types inheriting from FrameworkElement.",
                    source));
            }

            var newScope = (ValidationScope)args.NewValue;
            newScope.ScopeElement = scopeElement;
            newScope.ScopeElement.BindingValidationError += newScope.ScopeElementBindingValidationError;
            RegisterBindingErrorEvents(newScope.ScopeElement, newScope);
        }

        /// <summary>
        /// Handles the BindingValidationError event of the ScopeElement control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Windows.Controls.ValidationErrorEventArgs"/> instance containing the event data.</param>
        private void ScopeElementBindingValidationError(object sender, ValidationErrorEventArgs e)
        {
            if (e.Action == ValidationErrorEventAction.Removed)
            {
                this.Errors.Remove(e.Error);
            }
            else if (e.Action == ValidationErrorEventAction.Added)
            {
                this.Errors.Add(e.Error);
            }
        }
    }
}
